import { ClassConstructor, ClassTransformOptions, ClassTransformer } from 'class-transformer';
import { isMarkedSupportQuickTransform } from './SupportQuickTransform';

/**
 * These are the default options used by any transformation operation.
 */
const defaultOptions: Partial<ClassTransformOptions> = {
  enableCircularCheck: false,
  enableImplicitConversion: false,
  excludeExtraneousValues: false,
  excludePrefixes: undefined,
  exposeDefaultValues: false,
  exposeUnsetFields: true,
  groups: undefined,
  ignoreDecorators: false,
  strategy: undefined,
  targetMaps: undefined,
  version: undefined,
};

export class QuickTransformer {
  private static readonly OBJECT_PROTOTYPE = Object.getPrototypeOf({});
  private static readonly ARRAY_PROTOTYPE = Object.getPrototypeOf([]);

  private static isHooked = false;
  private static originPlainToInstance = ClassTransformer.prototype.plainToInstance;

  static installHook() {
    ClassTransformer.prototype.plainToInstance = QuickTransformer.plainToInstance;
    QuickTransformer.isHooked = true;
  }

  static uninstallHook() {
    ClassTransformer.prototype.plainToInstance = QuickTransformer.originPlainToInstance;
    QuickTransformer.isHooked = false;
  }

  static hookScope<T>(func: () => T): T {
    const originIsHooked = QuickTransformer.isHooked;
    if (!originIsHooked) {
      QuickTransformer.installHook();
    }
    const ret = func();
    if (!originIsHooked) {
      QuickTransformer.uninstallHook();
    }
    return ret;
  }

  static plainToInstance<T, V>(cls: ClassConstructor<T>, plain: V, options?: ClassTransformOptions): T;
  static plainToInstance<T, V>(cls: ClassConstructor<T>, plain: V[], options?: ClassTransformOptions): T[];
  static plainToInstance<T, V>(cls: ClassConstructor<T>, plain: V | V[], options?: ClassTransformOptions): T | T[] {
    if (Array.isArray(plain)) {
      return QuickTransformer.plainToInstanceArray(cls, plain, options);
    }
    return QuickTransformer.plainToInstanceSingle(cls, plain, options);
  }

  private static plainToInstanceArray<T, V>(cls: ClassConstructor<T>, plains: V[], options?: ClassTransformOptions): T[] {
    if (!plains) {
      return plains as any;
    }
    const length = plains.length;
    const objs = new Array(length);
    for (let i = 0; i < length; i++) {
      objs[i] = QuickTransformer.plainToInstanceSingle(cls, plains[i], options);
    }
    return objs;
  }

  private static plainToInstanceSingle<T, V>(cls: ClassConstructor<T>, plain: V, options?: ClassTransformOptions): T {
    const funcName = `to${cls.name}`;
    if (QuickTransformer[funcName]) {
      return QuickTransformer[funcName](plain);
    }
    if (!plain) {
      return plain as any;
    }
    if (!QuickTransformer.hasOwnQuickTransformFunc(cls)) {
      return QuickTransformer.originPlainToInstance(cls, plain, options);
    }
    const that = (options && options.excludeExtraneousValues)
      ? new cls()
      : Object.assign(new cls(), plain);
    cls['__fromPlain__'](plain, that, options);
    return that;
  }

  private static toString(value: any): any {
    if (!value) {
      return value;
    }
    try {
      return String(value);
    } catch(err) {
      return JSON.stringify(value);
    }
  }

  private static toNumber(value: any): any {
    if (!value) {
      return value;
    }
    return Number(value);
  }

  private static toBoolean(value: any): any {
    if (!value) {
      return value;
    }
    return Boolean(value);
  }

  private static toMap(value: any): any {
    if (!value) {
      return value;
    }
    const that = new Map();
    for (const key of Object.keys(value)) {
      that.set(key, value[key]);
    }
    return that;
  }

  static toObjectIfNeed(plain: any): any {
    if (!plain) {
      return plain;
    }
    if (Array.isArray(plain)) {
      return QuickTransformer.setPrototypeIfNeed(plain, QuickTransformer.ARRAY_PROTOTYPE);
    }
    if ('object' === typeof plain) {
      return QuickTransformer.setPrototypeIfNeed(plain, QuickTransformer.OBJECT_PROTOTYPE);
    }
    return plain;
  }

  static toArrayIfNeed(plain: any): any {
    if (!plain || !Array.isArray(plain)) {
      return plain;
    }
    return QuickTransformer.setPrototypeIfNeed(plain, QuickTransformer.ARRAY_PROTOTYPE);
  }

  private static setPrototypeIfNeed(o: any, proto: object): any {
    if (Object.getPrototypeOf(o)) {
      return o;
    }
    return Object.setPrototypeOf(o, proto);
  }

  static enhanceOptions(options?: ClassTransformOptions): ClassTransformOptions {
    return {
      ...defaultOptions,
      ...options,
    };
  }

  static supportQuickTransform<T>(cls: ClassConstructor<T>): boolean {
    return isMarkedSupportQuickTransform(cls) ||
      QuickTransformer.hasOwnQuickTransformFunc(cls);
  }

  private static hasOwnQuickTransformFunc<T>(cls: ClassConstructor<T>): boolean {
    if (!cls) {
      return false;
    }
    // adapt @Observed, since Observed decorator return a anonymous sub class
    if (cls.name) {
      return cls.hasOwnProperty('__fromPlain__');
    }
    return Object.getPrototypeOf(cls)?.hasOwnProperty('__fromPlain__') ?? false;
  }

  static assert(msg: string) {
    const err = new Error(msg);
    // setTimeout for prevent unhandled promise reject case
    setTimeout(() => {
      throw err;
    }, 0);
    // throw immediately for terminate current call stack
    throw err;
  }
}
